package com.jorado.search.solr.client.inner;

import org.apache.http.client.HttpClient;
import org.apache.solr.client.solrj.ResponseParser;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.BinaryResponseParser;
import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.common.params.ModifiableSolrParams;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

public class LBHttpSolrClient extends LBSolrClient {

    private final HttpClient httpClient;
    private final boolean clientIsInternal;
    private final ConcurrentHashMap<String, HttpSolrClient> urlToClient = new ConcurrentHashMap<>();
    private final HttpSolrClient.Builder httpSolrClientBuilder;

    private Integer connectionTimeout;
    private volatile Integer soTimeout;

    /**
     * @deprecated use {@link LBSolrClient.Req} instead
     */
    @Deprecated
    public static class Req extends LBSolrClient.Req {
        public Req(SolrRequest request, List<String> servers) {
            super(request, servers);
        }

        public Req(SolrRequest request, List<String> servers, Integer numServersToTry) {
            super(request, servers, numServersToTry);
        }
    }

    /**
     * @deprecated use {@link LBSolrClient.Rsp} instead
     */
    @Deprecated
    public static class Rsp extends LBSolrClient.Rsp {

    }

    /**
     * The provided httpClient should use a multi-threaded connection manager
     *
     * @deprecated use {@link LBHttpSolrClient#LBHttpSolrClient(Builder)} instead, as it is a more extension/subclassing-friendly alternative
     */
    @Deprecated
    protected LBHttpSolrClient(HttpSolrClient.Builder httpSolrClientBuilder,
                               HttpClient httpClient, String... solrServerUrl) {
        this(new Builder()
                .withHttpSolrClientBuilder(httpSolrClientBuilder)
                .withHttpClient(httpClient)
                .withBaseSolrUrls(solrServerUrl));
    }

    /**
     * The provided httpClient should use a multi-threaded connection manager
     *
     * @deprecated use {@link LBHttpSolrClient#LBHttpSolrClient(Builder)} instead, as it is a more extension/subclassing-friendly alternative
     */
    @Deprecated
    protected LBHttpSolrClient(HttpClient httpClient, ResponseParser parser, String... solrServerUrl) {
        this(new Builder()
                .withBaseSolrUrls(solrServerUrl)
                .withResponseParser(parser)
                .withHttpClient(httpClient));
    }

    protected LBHttpSolrClient(Builder builder) {
        super(builder.baseSolrUrls);
        this.clientIsInternal = builder.httpClient == null;
        this.httpSolrClientBuilder = builder.httpSolrClientBuilder;
        this.httpClient = builder.httpClient == null ? constructClient(builder.baseSolrUrls.toArray(new String[builder.baseSolrUrls.size()])) : builder.httpClient;
        this.connectionTimeout = builder.connectionTimeoutMillis;
        this.soTimeout = builder.socketTimeoutMillis;
        this.parser = builder.responseParser;
        for (String baseUrl : builder.baseSolrUrls) {
            urlToClient.put(baseUrl, makeSolrClient(baseUrl));
        }
    }

    private HttpClient constructClient(String[] solrServerUrl) {
        ModifiableSolrParams params = new ModifiableSolrParams();
        if (solrServerUrl != null && solrServerUrl.length > 1) {
            // we prefer retrying another server
            params.set(HttpClientUtil.PROP_USE_RETRY, false);
        } else {
            params.set(HttpClientUtil.PROP_USE_RETRY, true);
        }
        return HttpClientUtil.createClient(params);
    }

    protected HttpSolrClient makeSolrClient(String server) {
        HttpSolrClient client;
        if (httpSolrClientBuilder != null) {
            synchronized (this) {
                httpSolrClientBuilder
                        .withBaseSolrUrl(server)
                        .withHttpClient(httpClient);
                if (connectionTimeout != null) {
                    httpSolrClientBuilder.withConnectionTimeout(connectionTimeout);
                }
                if (soTimeout != null) {
                    httpSolrClientBuilder.withSocketTimeout(soTimeout);
                }
                client = httpSolrClientBuilder.build();
            }
        } else {
            final HttpSolrClient.Builder clientBuilder = new HttpSolrClient.Builder(server)
                    .withHttpClient(httpClient)
                    .withResponseParser(parser);
            if (connectionTimeout != null) {
                clientBuilder.withConnectionTimeout(connectionTimeout);
            }
            if (soTimeout != null) {
                clientBuilder.withSocketTimeout(soTimeout);
            }
            client = clientBuilder.build();
        }
        if (requestWriter != null) {
            client.setRequestWriter(requestWriter);
        }
        if (queryParams != null) {
            client.setQueryParams(queryParams);
        }
        return client;
    }

    /**
     * @deprecated since 7.0  Use {@link Builder} methods instead.
     */
    @Deprecated
    public void setConnectionTimeout(int timeout) {
        this.connectionTimeout = timeout;
        this.urlToClient.values().forEach(client -> client.setConnectionTimeout(timeout));
    }

    /**
     * set soTimeout (read timeout) on the underlying HttpConnectionManager. This is desirable for queries, but probably
     * not for indexing.
     *
     * @deprecated since 7.0  Use {@link Builder} methods instead.
     */
    @Deprecated
    public void setSoTimeout(int timeout) {
        this.soTimeout = timeout;
        this.urlToClient.values().forEach(client -> client.setSoTimeout(timeout));
    }

    /**
     * @deprecated use {@link LBSolrClient#request(LBSolrClient.Req)} instead
     */
    @Deprecated
    public Rsp request(Req req) throws SolrServerException, IOException {
        LBSolrClient.Rsp rsp = super.request(req);
        // for backward-compatibility support
        Rsp result = new Rsp();
        result.rsp = rsp.rsp;
        result.server = rsp.server;
        return result;
    }

    @Override
    protected SolrClient getClient(String baseUrl) {
        HttpSolrClient client = urlToClient.get(baseUrl);
        if (client == null) {
            return makeSolrClient(baseUrl);
        } else {
            return client;
        }
    }

    @Override
    public String removeSolrServer(String server) {
        urlToClient.remove(server);
        return super.removeSolrServer(server);
    }

    @Override
    public void close() {
        super.close();
        if (clientIsInternal) {
            HttpClientUtil.close(httpClient);
        }
    }

    /**
     * Return the HttpClient this instance uses.
     */
    public HttpClient getHttpClient() {
        return httpClient;
    }

    /**
     * Constructs {@link LBHttpSolrClient} instances from provided configuration.
     */
    public static class Builder extends SolrClientBuilder<Builder> {
        protected final List<String> baseSolrUrls;
        protected HttpSolrClient.Builder httpSolrClientBuilder;

        public Builder() {
            this.baseSolrUrls = new ArrayList<>();
            this.responseParser = new BinaryResponseParser();
        }

        public HttpSolrClient.Builder getHttpSolrClientBuilder() {
            return httpSolrClientBuilder;
        }

        /**
         * Provide a Solr endpoint to be used when configuring {@link LBHttpSolrClient} instances.
         * <p>
         * Method may be called multiple times.  All provided values will be used.
         * <p>
         * Two different paths can be specified as a part of the URL:
         * <p>
         * 1) A path pointing directly at a particular core
         * <pre>
         *   SolrClient client = builder.withBaseSolrUrl("http://my-solr-server:8983/solr/core1").build();
         *   QueryResponse resp = client.query(new SolrQuery("*:*"));
         * </pre>
         * Note that when a core is provided in the base URL, queries and other requests can be made without mentioning the
         * core explicitly.  However, the client can only send requests to that core.
         * <p>
         * 2) The path of the root Solr path ("/solr")
         * <pre>
         *   SolrClient client = builder.withBaseSolrUrl("http://my-solr-server:8983/solr").build();
         *   QueryResponse resp = client.query("core1", new SolrQuery("*:*"));
         * </pre>
         * In this case the client is more flexible and can be used to send requests to any cores.  This flexibility though
         * requires that the core is specified on all requests.
         */
        public Builder withBaseSolrUrl(String baseSolrUrl) {
            this.baseSolrUrls.add(baseSolrUrl);
            return this;
        }

        /**
         * Provide Solr endpoints to be used when configuring {@link LBHttpSolrClient} instances.
         * <p>
         * Method may be called multiple times.  All provided values will be used.
         * <p>
         * Two different paths can be specified as a part of each URL:
         * <p>
         * 1) A path pointing directly at a particular core
         * <pre>
         *   SolrClient client = builder.withBaseSolrUrls("http://my-solr-server:8983/solr/core1").build();
         *   QueryResponse resp = client.query(new SolrQuery("*:*"));
         * </pre>
         * Note that when a core is provided in the base URL, queries and other requests can be made without mentioning the
         * core explicitly.  However, the client can only send requests to that core.
         * <p>
         * 2) The path of the root Solr path ("/solr")
         * <pre>
         *   SolrClient client = builder.withBaseSolrUrls("http://my-solr-server:8983/solr").build();
         *   QueryResponse resp = client.query("core1", new SolrQuery("*:*"));
         * </pre>
         * In this case the client is more flexible and can be used to send requests to any cores.  This flexibility though
         * requires that the core is specified on all requests.
         */
        public Builder withBaseSolrUrls(String... solrUrls) {
            for (String baseSolrUrl : solrUrls) {
                this.baseSolrUrls.add(baseSolrUrl);
            }
            return this;
        }

        /**
         * Provides a {@link HttpSolrClient.Builder} to be used for building the internally used clients.
         */
        public Builder withHttpSolrClientBuilder(HttpSolrClient.Builder builder) {
            this.httpSolrClientBuilder = builder;
            return this;
        }

        /**
         * Create a {@link HttpSolrClient} based on provided configuration.
         */
        public LBHttpSolrClient build() {
            return new LBHttpSolrClient(this);
        }

        @Override
        public Builder getThis() {
            return this;
        }
    }
}
